// TwoGtp.java

package net.sf.gogui.tools.twogtp;

import java.util.ArrayList;
import net.sf.gogui.game.ConstNode;
import net.sf.gogui.game.ConstGameTree;
import net.sf.gogui.game.Game;
import net.sf.gogui.game.NodeUtil;
import net.sf.gogui.game.TimeSettings;
import net.sf.gogui.go.BlackWhiteSet;
import net.sf.gogui.go.ConstBoard;
import net.sf.gogui.go.GoColor;
import static net.sf.gogui.go.GoColor.BLACK;
import static net.sf.gogui.go.GoColor.WHITE;
import net.sf.gogui.go.GoPoint;
import net.sf.gogui.go.InvalidKomiException;
import net.sf.gogui.go.Komi;
import net.sf.gogui.go.Move;
import net.sf.gogui.gtp.GtpCommand;
import net.sf.gogui.gtp.GtpEngine;
import net.sf.gogui.gtp.GtpError;
import net.sf.gogui.gtp.GtpResponseFormatError;
import net.sf.gogui.gtp.GtpUtil;
import net.sf.gogui.util.ErrorMessage;
import net.sf.gogui.util.ObjectUtil;
import net.sf.gogui.util.Platform;
import net.sf.gogui.util.StringUtil;
import net.sf.gogui.version.Version;

/** GTP adapter for playing games between two Go programs. */
public class TwoGtp
    extends GtpEngine
{
    /** Constructor.
        @param komi The fixed komi. See TwoGtp documentation for option
        -komi */
    public TwoGtp(Program black, Program white, Program referee,
                  String observer, int size, Komi komi, int numberGames,
                  boolean alternate, String filePrefix, boolean verbose,
                  Openings openings, TimeSettings timeSettings,
                  ResultFile resultFile)
        throws Exception
    {
        super(null);
        assert size > 0;
        assert size <= GoPoint.MAX_SIZE;
        assert komi != null;
        if (black.equals(""))
            throw new ErrorMessage("No black program set");
        if (white.equals(""))
            throw new ErrorMessage("No white program set");
        m_filePrefix = filePrefix;
        m_allPrograms = new ArrayList<Program>();
        m_black = black;
        m_allPrograms.add(m_black);
        m_white = white;
        m_allPrograms.add(m_white);
        m_referee = referee;
        if (m_referee != null)
            m_allPrograms.add(m_referee);
        if (observer.equals(""))
            m_observer = null;
        else
        {
            m_observer = new Program(observer, "Observer", "O", verbose);
            m_allPrograms.add(m_observer);
        }
        for (Program program : m_allPrograms)
            program.setLabel(m_allPrograms);
        m_size = size;
        m_komi = komi;
        m_alternate = alternate;
        m_numberGames = numberGames;
        m_openings = openings;
        m_verbose = verbose;
        m_timeSettings = timeSettings;
        m_resultFile = resultFile;
        initGame(size);
    }

    public void autoPlay() throws Exception
    {
        StringBuilder response = new StringBuilder(256);
        while (true)
        {
            try
            {
                newGame(m_size);
                while (! gameOver())
                {
                    response.setLength(0);
                    sendGenmove(getToMove(), response);
                }
            }
            catch (GtpError e)
            {
                if (m_gameIndex == -1)
                    break;
                handleEndOfGame(true, e.getMessage());
            }
        }
        if (m_black.isProgramDead())
            throw new ErrorMessage("Black program died");
        if (m_white.isProgramDead())
            throw new ErrorMessage("White program died");
    }

    public void close()
    {
        for (Program program : m_allPrograms)
            program.close();
    }

    public void handleCommand(GtpCommand cmd) throws GtpError
    {
        String command = cmd.getCommand();
        if (command.equals("boardsize"))
            cmdBoardSize(cmd);
        else if (command.equals("clear_board"))
            cmdClearBoard(cmd);
        else if (command.equals("final_score"))
            finalStatusCommand(cmd);
        else if (command.equals("final_status"))
            finalStatusCommand(cmd);
        else if (command.equals("final_status_list"))
            finalStatusCommand(cmd);
        else if (command.equals("gogui-interrupt"))
            ;
        else if (command.equals("gogui-title"))
            cmd.setResponse(getTitle());
        else if (command.equals("gogui-twogtp-black"))
            twogtpColor(m_black, cmd);
        else if (command.equals("gogui-twogtp-white"))
            twogtpColor(m_white, cmd);
        else if (command.equals("gogui-twogtp-referee"))
            twogtpReferee(cmd);
        else if (command.equals("gogui-twogtp-observer"))
            twogtpObserver(cmd);
        else if (command.equals("quit"))
        {
            close();
            setQuit();
        }
        else if (command.equals("play"))
            cmdPlay(cmd);
        else if (command.equals("undo"))
            cmdUndo(cmd);
        else if (command.equals("genmove"))
            cmdGenmove(cmd);
        else if (command.equals("komi"))
            komi(cmd);
        else if (command.equals("scoring_system"))
            sendIfSupported(command, cmd.getLine());
        else if (command.equals("name"))
            cmd.setResponse("gogui-twogtp");
        else if (command.equals("version"))
            cmd.setResponse(Version.get());
        else if (command.equals("protocol_version"))
            cmd.setResponse("2");
        else if (command.equals("list_commands"))
            cmd.setResponse("boardsize\n" +
                            "clear_board\n" +
                            "final_score\n" +
                            "final_status\n" +
                            "final_status_list\n" +
                            "genmove\n" +
                            "gogui-interrupt\n" +
                            "gogui-title\n" +
                            "komi\n" +
                            "list_commands\n" +
                            "name\n" +
                            "play\n" +
                            "quit\n" +
                            "scoring_system\n" +
                            "time_settings\n" +
                            "gogui-twogtp-black\n" +
                            "gogui-twogtp-observer\n" +
                            "gogui-twogtp-referee\n" +
                            "gogui-twogtp-white\n" +
                            "undo\n" +
                            "version\n");
        else if (GtpUtil.isStateChangingCommand(command))
            throw new GtpError("unknown command");
        else if (command.equals("time_settings"))
            sendIfSupported(command, cmd.getLine());
        else
        {
            boolean isExtCommandBlack = m_black.isSupported(command);
            boolean isExtCommandWhite = m_white.isSupported(command);
            boolean isExtCommandReferee = false;
            if (m_referee != null)
                isExtCommandReferee = m_referee.isSupported(command);
            boolean isExtCommandObserver = false;
            if (m_observer != null)
                isExtCommandObserver = m_observer.isSupported(command);
            if (isExtCommandBlack && ! isExtCommandObserver
                && ! isExtCommandWhite && ! isExtCommandReferee)
                forward(m_black, cmd);
            if (isExtCommandWhite && ! isExtCommandObserver
                && ! isExtCommandBlack && ! isExtCommandReferee)
                forward(m_white, cmd);
            if (isExtCommandReferee && ! isExtCommandObserver
                && ! isExtCommandBlack && ! isExtCommandWhite)
                forward(m_referee, cmd);
            if (isExtCommandObserver && ! isExtCommandReferee
                && ! isExtCommandBlack && ! isExtCommandWhite)
                forward(m_observer, cmd);
            if (! isExtCommandReferee
                && ! isExtCommandBlack
                && ! isExtCommandObserver
                && ! isExtCommandWhite)
                throw new GtpError("unknown command");
            throw new GtpError("use gogui-twogtp-black/white/referee/observer");
        }
    }

    public void interruptCommand()
    {
        for (Program program : m_allPrograms)
            program.interruptProgram();
    }

    /** Limit number of moves.
        @param maxMoves Maximum number of moves after which genmove will fail,
        -1 for no limit. */
    public void setMaxMoves(int maxMoves)
    {
        m_maxMoves = maxMoves;
    }

    private final boolean m_alternate;

    private boolean m_gameSaved;

    private int m_maxMoves = 1000;

    private int m_gameIndex;

    private boolean m_resigned;

    private final boolean m_verbose;

    private final int m_numberGames;

    private final int m_size;

    /** Fixed komi. */
    private final Komi m_komi;

    private Game m_game;

    private GoColor m_resignColor;

    private final Openings m_openings;

    private final Program m_black;

    private final Program m_white;

    private final Program m_referee;

    private final Program m_observer;

    private final ArrayList<Program> m_allPrograms;

    private final BlackWhiteSet<Double> m_realTime =
        new BlackWhiteSet<Double>(0., 0.);

    private String m_openingFile;

    private final String m_filePrefix;

    private final ArrayList<ArrayList<Compare.Placement>> m_games
        = new ArrayList<ArrayList<Compare.Placement>>(100);

    private ResultFile m_resultFile;

    private final TimeSettings m_timeSettings;

    private ConstNode m_lastOpeningNode;

    private void checkInconsistentState() throws GtpError
    {
        for (Program program : m_allPrograms)
            if (program.isOutOfSync())
                throw new GtpError("Inconsistent state");
    }

    private void cmdBoardSize(GtpCommand cmd) throws GtpError
    {
        cmd.checkNuArg(1);
        int size = cmd.getIntArg(0, 1, GoPoint.MAX_SIZE);
        if (size != m_size)
            throw new GtpError("Size must be " + m_size);
    }

    private void cmdClearBoard(GtpCommand cmd) throws GtpError
    {
        cmd.checkArgNone();
        newGame(m_size);
    }

    private void cmdGenmove(GtpCommand cmd) throws GtpError
    {
        try
        {
            sendGenmove(cmd.getColorArg(), cmd.getResponse());
        }
        catch (ErrorMessage e)
        {
            throw new GtpError(e.getMessage());
        }
    }

    private void cmdPlay(GtpCommand cmd) throws GtpError
    {
        cmd.checkNuArg(2);
        checkInconsistentState();
        GoColor color = cmd.getColorArg(0);
        GoPoint point = cmd.getPointArg(1, m_size);
        Move move = Move.get(color, point);
        m_game.play(move);
        synchronize();
    }

    private void cmdUndo(GtpCommand cmd) throws GtpError
    {
        cmd.checkArgNone();
        int moveNumber = m_game.getMoveNumber();
        if (moveNumber == 0)
            throw new GtpError("cannot undo");
        m_game.gotoNode(getCurrentNode().getFatherConst());
        assert m_game.getMoveNumber() == moveNumber - 1;
        synchronize();
    }

    private void finalStatusCommand(GtpCommand cmd) throws GtpError
    {
        checkInconsistentState();
        if (m_referee != null)
            forward(m_referee, cmd);
        else if (m_black.isSupported("final_status"))
            forward(m_black, cmd);
        else if (m_white.isSupported("final_status"))
            forward(m_white, cmd);
        else
            throw new GtpError("neither player supports final_status");
    }

    private void forward(Program program, GtpCommand cmd) throws GtpError
    {
        cmd.setResponse(program.send(cmd.getLine()));
    }

    private boolean gameOver()
    {
        return (getBoard().bothPassed() || m_resigned);
    }

    private ConstBoard getBoard()
    {
        return m_game.getBoard();
    }

    private ConstNode getCurrentNode()
    {
        return m_game.getCurrentNode();
    }

    private GoColor getToMove()
    {
        return m_game.getToMove();
    }

    private ConstGameTree getTree()
    {
        return m_game.getTree();
    }

    private String getTitle()
    {
        StringBuilder buffer = new StringBuilder();
        String nameBlack = m_black.getLabel();
        String nameWhite = m_white.getLabel();
        if (isAlternated())
        {
            String tmpName = nameBlack;
            nameBlack = nameWhite;
            nameWhite = tmpName;
        }
        buffer.append(nameWhite);
        buffer.append(" vs ");
        buffer.append(nameBlack);
        buffer.append(" (B)");
        if (! m_filePrefix.equals(""))
        {
            buffer.append(" (");
            buffer.append(m_gameIndex + 1);
            buffer.append(')');
        }
        return buffer.toString();
    }

    private void handleEndOfGame(boolean error, String errorMessage)
        throws ErrorMessage
    {
        String resultBlack;
        String resultWhite;
        String resultReferee;
        if (m_resigned)
        {
            String result = (m_resignColor == BLACK ? "W" : "B");
            result = result + "+R";
            resultBlack = result;
            resultWhite = result;
            resultReferee = result;
        }
        else
        {
            resultBlack = m_black.getResult();
            resultWhite = m_white.getResult();
            resultReferee = "?";
            if (m_referee != null)
                resultReferee = m_referee.getResult();
        }
        double cpuTimeBlack = m_black.getAndClearCpuTime();
        double cpuTimeWhite = m_white.getAndClearCpuTime();
        double realTimeBlack = m_realTime.get(BLACK);
        double realTimeWhite = m_realTime.get(WHITE);
        if (isAlternated())
        {
            resultBlack = inverseResult(resultBlack);
            resultWhite = inverseResult(resultWhite);
            resultReferee = inverseResult(resultReferee);
            realTimeBlack = m_realTime.get(WHITE);
            realTimeWhite = m_realTime.get(BLACK);
        }
        // If a program is dead we wait for a few seconds, because it
        // could be because the TwoGtp process was killed and we don't
        // want to write a result in this case.
        if (m_black.isProgramDead() || m_white.isProgramDead())
        {
            try
            {
                Thread.sleep(3000);
            }
            catch (InterruptedException e)
            {
                assert false;
            }
        }

        String nameBlack = m_black.getLabel();
        String nameWhite = m_white.getLabel();
        String blackCommand = m_black.getProgramCommand();
        String whiteCommand = m_white.getProgramCommand();
        String blackVersion = m_black.getVersion();
        String whiteVersion = m_white.getVersion();
        if (isAlternated())
        {
            nameBlack = m_white.getLabel();
            nameWhite = m_black.getLabel();
            blackCommand = m_white.getProgramCommand();
            whiteCommand = m_black.getProgramCommand();
            blackVersion = m_white.getVersion();
            whiteVersion = m_black.getVersion();
            String resultTmp = inverseResult(resultWhite);
            resultWhite = inverseResult(resultBlack);
            resultBlack = resultTmp;
            resultReferee = inverseResult(resultReferee);
        }
        m_game.setPlayer(BLACK, nameBlack);
        m_game.setPlayer(WHITE, nameWhite);
        if (m_referee != null)
            m_game.setResult(resultReferee);
        else if (resultBlack.equals(resultWhite) && ! resultBlack.equals("?"))
            m_game.setResult(resultBlack);
        String host = Platform.getHostInfo();
        StringBuilder comment = new StringBuilder();
        comment.append("Black command: ");
        comment.append(blackCommand);
        comment.append("\nWhite command: ");
        comment.append(whiteCommand);
        comment.append("\nBlack version: ");
        comment.append(blackVersion);
        comment.append("\nWhite version: ");
        comment.append(whiteVersion);
        if (m_openings != null)
        {
            comment.append("\nOpening: ");
            comment.append(m_openingFile);
        }
        comment.append("\nResult[Black]: ");
        comment.append(resultBlack);
        comment.append("\nResult[White]: ");
        comment.append(resultWhite);
        if (m_referee != null)
        {
            comment.append("\nReferee: ");
            comment.append(m_referee.getProgramCommand());
            comment.append("\nResult[Referee]: ");
            comment.append(resultReferee);
        }
        comment.append("\nHost: ");
        comment.append(host);
        comment.append("\nDate: ");
        comment.append(StringUtil.getDate());
        m_game.setComment(comment.toString(), getTree().getRootConst());
        int moveNumber = NodeUtil.getMoveNumber(getCurrentNode());
        m_resultFile.addResult(m_gameIndex, m_game, resultBlack, resultWhite,
                               resultReferee, isAlternated(), moveNumber,
                               error, errorMessage, realTimeBlack,
                               realTimeWhite, cpuTimeBlack, cpuTimeWhite);
    }

    private void initGame(int size) throws GtpError
    {
        m_game = new Game(size, m_komi, null, null, null);
        m_realTime.set(BLACK, 0.);
        m_realTime.set(WHITE, 0.);
        // Clock is not needed
        m_game.haltClock();
        m_resigned = false;
        if (m_openings != null)
        {
            int openingFileIndex;
            if (m_alternate)
                openingFileIndex = (m_gameIndex / 2) % m_openings.getNumber();
            else
                openingFileIndex = m_gameIndex % m_openings.getNumber();
            try
            {
                m_openings.loadFile(openingFileIndex);
            }
            catch (Exception e)
            {
                throw new GtpError(e.getMessage());
            }
            m_openingFile = m_openings.getFilename();
            if (m_verbose)
                System.err.println("Loaded opening " + m_openingFile);
            if (m_openings.getBoardSize() != size)
                throw new GtpError("Wrong board size: " + m_openingFile);
            m_game.init(m_openings.getTree());
            m_game.setKomi(m_komi);
            m_lastOpeningNode = NodeUtil.getLast(getTree().getRootConst());
            // TODO: Check that root node contains no setup stones, if
            // TwoGtp is run as a GTP engine, see also comment in sendGenmove()
        }
        else
            m_lastOpeningNode = null;
        synchronizeInit();
    }

    private String inverseResult(String result)
    {
        if (result.indexOf('B') >= 0)
            return result.replaceAll("B", "W");
        else if (result.indexOf('W') >= 0)
            return result.replaceAll("W", "B");
        else
            return result;
    }

    private boolean isAlternated()
    {
        return (m_alternate && m_gameIndex % 2 != 0);
    }

    private boolean isInOpening()
    {
        if (m_lastOpeningNode == null)
            return false;
        for (ConstNode node = getCurrentNode().getChildConst(); node != null;
             node = node.getChildConst())
            if (node == m_lastOpeningNode)
                return true;
        return false;
    }

    private void komi(GtpCommand cmd) throws GtpError
    {
        String arg = cmd.getArg();
        try
        {
            Komi komi = Komi.parseKomi(arg);
            if (! ObjectUtil.equals(komi, m_komi))
                throw new GtpError("komi is fixed at " + m_komi);
        }
        catch (InvalidKomiException e)
        {
            throw new GtpError("invalid komi: " + arg);
        }
    }

    private void newGame(int size) throws GtpError
    {
        if (m_resultFile != null)
            m_gameIndex = m_resultFile.getNextGameIndex();
        else
        {
            ++m_gameIndex;
            if (m_numberGames > 0 && m_gameIndex > m_numberGames)
                m_gameIndex = -1;
        }
        if (m_gameIndex == -1)
            throw new GtpError("maximum number of games reached");
        if (m_verbose)
        {
            System.err.println("============================================");
            System.err.println("Game " + m_gameIndex);
            System.err.println("============================================");
        }
        m_black.getAndClearCpuTime();
        m_white.getAndClearCpuTime();
        initGame(size);
        m_gameSaved = false;
        if (m_timeSettings != null)
            sendIfSupported("time_settings",
                            GtpUtil.getTimeSettingsCommand(m_timeSettings));
    }

    private void sendGenmove(GoColor color, StringBuilder response)
        throws GtpError, ErrorMessage
    {
        checkInconsistentState();
        int moveNumber = m_game.getMoveNumber();
        if (m_maxMoves >= 0 && moveNumber > m_maxMoves)
            throw new GtpError("move limit exceeded");
        if (isInOpening())
        {
            // TODO: Check that node contains no setup stones or fully support
            // openings with setup stones and non-alternating moves in GTP
            // engine mode again (by transforming the opening file into a
            // sequence of alternating moves, replacing setup stones by moves
            // and filling in passes). See also comment in initGame() and
            // doc/manual/xml/reference-twogtp.xml
            ConstNode child = getCurrentNode().getChildConst();
            Move move = child.getMove();
            if (move.getColor() != color)
                throw new GtpError("next opening move is " + move);
            m_game.gotoNode(child);
            synchronize();
            response.append(GoPoint.toString(move.getPoint()));
            return;
        }
        Program program;
        boolean exchangeColors =
            (color == BLACK && isAlternated())
            || (color == WHITE && ! isAlternated());
        if (exchangeColors)
            program = m_white;
        else
            program = m_black;
        long timeMillis = System.currentTimeMillis();
        String responseGenmove = program.sendCommandGenmove(color);
        double time = (System.currentTimeMillis() - timeMillis) / 1000.;
        m_realTime.set(color, m_realTime.get(color) + time);
        if (responseGenmove.equalsIgnoreCase("resign"))
        {
            response.append("resign");
            m_resigned = true;
            m_resignColor = color;
        }
        else
        {
            ConstBoard board = getBoard();
            GoPoint point = null;
            try
            {
                point = GtpUtil.parsePoint(responseGenmove, board.getSize());
            }
            catch (GtpResponseFormatError e)
            {
                throw new GtpError(program.getLabel()
                                   + " played invalid move: "
                                   + responseGenmove);
            }
            Move move = Move.get(color, point);
            m_game.play(move);
            program.updateAfterGenmove(board);
            synchronize();
            response.append(GoPoint.toString(move.getPoint()));
        }
        if (gameOver() && ! m_gameSaved)
        {
            handleEndOfGame(false, "");
            m_gameSaved = true;
        }
    }

    private void sendIfSupported(String cmd, String cmdLine)
    {
        for (Program program : m_allPrograms)
            program.sendIfSupported(cmd, cmdLine);
    }

    private void synchronize() throws GtpError
    {
        for (Program program : m_allPrograms)
            program.synchronize(m_game);
    }

    private void synchronizeInit() throws GtpError
    {
        for (Program program : m_allPrograms)
            program.synchronizeInit(m_game);
    }

    private void twogtpColor(Program program, GtpCommand cmd) throws GtpError
    {
        cmd.setResponse(program.send(cmd.getArgLine()));
    }

    private void twogtpObserver(GtpCommand cmd) throws GtpError
    {
        if (m_observer == null)
            throw new GtpError("no observer enabled");
        twogtpColor(m_observer, cmd);
    }

    private void twogtpReferee(GtpCommand cmd) throws GtpError
    {
        if (m_referee == null)
            throw new GtpError("no referee enabled");
        twogtpColor(m_referee, cmd);
    }
}
